var PNG = require('pngjs').PNG;
var fs = require('fs');

var Color = function(r, g, b) {
	this.r = r;
	this.g = g;
	this.b = b;
};

Color.prototype.add = function(c) {
	return new Color(this.r + c.r, this.g + c.g, this.b + c.b);
};

Color.prototype.multiply = function(s) {
	return new Color(this.r * s, this.g * s, this.b * s);
};

Color.prototype.modulate = function(c) {
	return new Color(this.r * c.r, this.g * c.g, this.b * c.b);
};

Color.black = new Color(0, 0, 0);
Color.white = new Color(1, 1, 1);
Color.red = new Color(1, 0, 0);
Color.green = new Color(0, 1, 0);
Color.blue = new Color(0, 0, 1);

Vector3 = function(x, y, z) {
	this.x = x;
	this.y = y;
	this.z = z;
};

Vector3.prototype.normalize = function() {
	var inv = 1 / Math.sqrt(this.dot(this));
	return new Vector3(this.x * inv, this.y * inv, this.z * inv);
};

Vector3.prototype.negate = function() {
	return new Vector3(-this.x, -this.y, -this.z);
};

Vector3.prototype.add = function(v) {
	return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z);
};

Vector3.prototype.subtract = function(v) {
	return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z);
};

Vector3.prototype.multiply = function(f) {
	return new Vector3(this.x * f, this.y * f, this.z * f);
};

Vector3.prototype.divide = function(f) {
	var invf = 1 / f;
	return new Vector3(this.x * invf, this.y * invf, this.z * invf);
};

Vector3.prototype.dot = function(v) {
	return this.x * v.x + this.y * v.y + this.z * v.z;
};

Vector3.prototype.sequare = function() {
	return this.dot(this);
};

Vector3.prototype.cross = function(v) {
	return new Vector3(this.z * v.y - this.y * v.z, this.x * v.z - this.z * v.x, this.y * v.x - this.x * v.y);
//	return new Vector3(-this.z * v.y + this.y * v.z, this.z * v.x - this.x * v.z, -this.y * v.x + this.x * v.y);
};

Vector3.zero = new Vector3(0, 0, 0);

var Sphere = function(center, radius) {
	this.center = center;
	this.radius = radius;
};

Sphere.prototype.initialize = function() {
	this.sqrRadius = this.radius * this.radius;
};

Sphere.prototype.intersect = function(ray) {
	var v = ray.origin.subtract(this.center);
	var DdotV = ray.direction.dot(v);
	var a0 = v.sequare() - this.sqrRadius;
	var discr = DdotV * DdotV - a0;
	if (discr < 0) {
		return IntersectResult.noHit;
	}
	var result = new IntersectResult();
	var disSqrt = Math.sqrt(discr);
	var d1 = -DdotV - disSqrt;
	var d2 = -DdotV + disSqrt;
	if(d1 > 0.0001) {
		result.distance = d1;
		result.dir = 1;
	} else if(d2 > 0.0001) {
		result.distance = d2;
		result.dir = -1;
	} else {
		return IntersectResult.noHit;
	}
	result.position = ray.getPoint(result.distance);
	result.normal = result.position.subtract(this.center).normalize();
	if(result.dir == -1) {
		result.normal = result.normal.negate();
	}
	result.geometry = this;
	return result;
};

var Plane = function(normal, d) {
	this.normal = normal;
	this.d = d;
};

Plane.prototype.initialize = function() {
	var up;
	if(this.normal.x ==0 && this.normal.y == 0) {
		if(this.normal.z > 0) {
			up = new Vector3(0, -1, 0);
		} else {
			up = new Vector3(0, 1, 0);
		}
	}
	this.right = up.cross(this.normal);
	this.up = this.right.cross(this.normal).negate();
	this.position = this.normal.multiply(this.d);
};

Plane.prototype.intersect = function(ray) {
	var a = ray.direction.dot(this.normal);
	if(a >= 0) {
		return IntersectResult.noHit;
	}
	var d = this.normal.dot(ray.origin.subtract(this.position));
	var result = new IntersectResult();
	result.geometry = this;
	result.distance = -d / a;
	result.position = ray.getPoint(result.distance);
	result.normal = this.normal;
	result.tt = true;
	return result;
};

var IntersectResult = function(){
	this.geometry = null;
	this.distance = 0;
	this.position = Vector3.zero;
	this.normal = Vector3.zero;
};

IntersectResult.noHit = new IntersectResult();

/*
	r(t) = o + td (t > 0);
*/
var Ray3 = function(origin, direction) {
	this.origin = origin;
	this.direction = direction;
};

Ray3.prototype.getPoint = function(t) {
	return this.origin.add(this.direction.multiply(t));
};

var PerspectiveCamera = function(eye, front, refUp, fov) {
	this.eye = eye;
	this.front = front.normalize();
	this.refUp = refUp.normalize();
	this.fov = fov;
};

PerspectiveCamera.prototype.initialize = function() {
//	x_axis = up.cross( *this );
//	z_axis = x_axis.cross( *this ).inversion();
	this.right = this.refUp.cross(this.front);
	this.up = this.right.cross(this.front);
	this.fovScale = Math.tan(this.fov / 2 * Math.PI / 180) * 2;
};

PerspectiveCamera.prototype.generateRay = function(x, y) {
	var r = this.right.multiply(x * 0.5);
	var u = this.up.multiply(y * 0.5);
	return new Ray3(this.eye, this.front.add(r).add(u).normalize());
};

var CheckerMaterial = function(diffuse, specular, reflect, refract, scale) {
	this.specular = specular
	this.diffuse = diffuse;
	this.reflect = reflect;
	this.refract = refract;
	this.scale = scale;
	this.black = new Color(0.1, 0.1, 0.1);
};

CheckerMaterial.prototype.sample = function(ray, position, normal, scene) {
	var u = position.dot(scene.right);
	var v = position.dot(scene.up);
	return Math.abs((Math.floor(u * this.scale) + Math.floor(v * this.scale)) % 2) < 1 ? this.black : Color.white;
};

var PhoneMaterial = function(diffuse, specular, reflect, refract, color, rindex) {
	this.diffuse = diffuse;
	this.specular = specular;
	this.reflect = reflect;
	this.refract = refract;
	this.color = color
	this.rindex= rindex;
};

PhoneMaterial.prototype.sample = function(ray, position, normal, scene) {
	return this.color;
	var NdotL = normal.dot(lightDir);
	var H = (lightDir.subtract(ray.direction)).normalize();
	var NdotH = normal.dot(H);
	var diffuseTemp = this.diffuse.multiply(Math.max(NdotL, 0));
	var specularTemp = this.specular.multiply(Math.pow(Math.max(NdotH), this.shininess));
	return lightColor.modulate(diffuseTemp.add(specularTemp));
};

var LightSample = function(l, el) {
	this.l = l;
	this.el = el;
};

LightSample.zero = new LightSample(Vector3.zero, Color.black);

LightSample.prototype.initialize = function() {};

LightSample.prototype.shade = function(scene, position) {
	var ray = new Ray3(position, this.l.subtract(position).normalize());
	var result = scene.intersect(ray);
	if(result.geometry) {
		return 0;
	}
	return 1;
};

LightSample.prototype.intersect = function(ray) {return IntersectResult.noHit;};

var AreaLight = function(el, P0, P1, P2, P3) {
	this.el = el;
	this.P0 = P0;
	this.P1 = P1;
	this.P2 = P2;
	this.P3 = P3;
	this.initialize();
};

AreaLight.prototype.initialize = function() {
	this.N = (this.P0.subtract(this.P1)).cross(this.P0.subtract(this.P2)).normalize();
	//this.radius = this.P0 * this.N / (this.N * this.N);
	//this.radius = this.P0.dot(new Vector3(1 / this.N.x, 1 / this.N.y, 1 / this.N.z));
	this.radius = this.P0.dot(this.N) / (this.N.dot(this.N));
	if (this.radius < -0.00001) {
		this.N = this.N.negate();
		this.radius = -this.radius;
	}
	this.l = (this.P0.add(this.P2)).multiply(0.5);
	this.shade_x = [1 , 3 , 2 , 0 , 2 , 0 , 0 , 2 , 3 , 1 , 1 , 3 , 2 , 3 , 1 , 0];
	this.shade_y = [2 , 3 , 0 , 1 , 3 , 3 , 0 , 2 , 1 , 3 , 0 , 2 , 1 , 0 , 1 , 2];
	this.area = 0;
	this.area += Math.sqrt(this.P1.subtract(this.P0).cross(this.P2.subtract(this.P0)).sequare());
	this.area += Math.sqrt(this.P2.subtract(this.P0).cross(this.P3.subtract(this.P0)).sequare());
};

AreaLight.prototype.shade = function(scene, position) {
	var shade = 1;
	var A = this.P1.subtract(this.P0).multiply(0.25);
	var B = this.P3.subtract(this.P0).multiply(0.25);
	var rays = 16;
	for (var j = 0 ; j < rays; j++) {
		var aim = this.P0;
		aim = aim.add(A.multiply((this.shade_x[j & 15] + Math.random())));
		aim = aim.add(B.multiply((this.shade_y[j & 15] + Math.random())));
		var fra_shade = 1;
		var ray = new Ray3(position, aim.subtract(position).normalize());
		var result = scene.intersect(ray);
		if(result.geometry) {
			fra_shade = 0;
		}
		shade -= (1 - fra_shade) / rays;
	}
	return shade;
};

AreaLight.prototype.intersect = function(ray) {
	var d = this.N.dot(ray.direction);
	if(Math.abs(d) < 0.00001) {
		return IntersectResult.noHit;
	}
	var l = this.N.multiply(this.radius).subtract(ray.origin).dot(this.N) / d;
	if(l < 0.00001) {
		return IntersectResult.noHit;
	}
	var nP = ray.origin.add(ray.direction.multiply(l));
	
	var cnt = 0;
	//nP = new Vector3(0.4191062335009018, 1.5564296302380558, 0.8);
	cnt += Math.sqrt(nP.subtract(this.P0).cross(nP.subtract(this.P1)).sequare());
	cnt += Math.sqrt(nP.subtract(this.P1).cross(nP.subtract(this.P2)).sequare());
	cnt += Math.sqrt(nP.subtract(this.P2).cross(nP.subtract(this.P3)).sequare());
	cnt += Math.sqrt(nP.subtract(this.P3).cross(nP.subtract(this.P0)).sequare());
	if((cnt - this.area) > 0.0001) {
		return IntersectResult.noHit;
	}
	var result = new IntersectResult();
	result.geometry = this;
	//result.distance = nP.subtract(ray.origin).sequare();
	//result.position = nP;
	//result.normal = this.N;
	//result.tt = true;
	return result;
};

LightSample.prototype.diffuse = function(scene, position, normal, diffuse, color) {
	var shade = 1;
	var tempRay = new Ray3(position, this.l.subtract(position).normalize());
	var tempResult = scene.intersect(tempRay);
	if(tempResult.geometry) {
		shade = 0;
	}
	var L = this.l.subtract(position).normalize();
	var NdotL = normal.dot(L);
	return color.multiply(Math.max(NdotL * shade * diffuse, 0));
};

LightSample.prototype.specular = function(ray, position, normal, specular) {
	var L = this.l.subtract(position).normalize()
	var NdotL = normal.dot(L);
	var R =  L.subtract(normal.multiply(2 * NdotL));
	var NdotR = ray.direction.dot(R);
	if(NdotR > 0) {
		var secularTemp = Math.pow(NdotR, 20) * specular * shade;
		return this.el.multiply(secularTemp);
	}
	return new Color(0, 0, 0);
};

var DirectionLight = function(irradience, direction) {
	this.irradience = irradience;
	this.direction = direction;
	this.shadow = true;
};

DirectionLight.prototype.initialize = function() {
	this.l = this.direction.normalize().negate();
};

DirectionLight.prototype.sample = function(scene, position) {
	var p = position;
	if(this.shadow){
		var ray = new Ray3(position, this.l);
		var result = scene.intersect(ray);
		if(result.geometry && result.ect == -1){
			return LightSample.zero;
		}
	}
	return new LightSample(this.l, this.irradience);
};

var PointLight = function(intensity, position) {
	this.intensity = intensity;
	this.position = position;
	this.shadow = true;
};

PointLight.prototype.initialize = function() {};

PointLight.prototype.sample = function(scene, position) {
	var delta = this.position.subtract(position);
	var rr = delta.sequare();
	var r = Math.sqrt(rr);
	var l = delta.divide(r);
	if(this.shadow) {
		var ray = new Ray3(position, l);
		var result = scene.intersect(ray);
		if(result.geometry && result.distance <= r) {
			return LightSample.zero;
		}
	}
	return new LightSample(l, this.intensity.multiply(1 / rr));
};

var SpotLight = function(intensity, position, direction, theta, phi, falloff) {
	this.intensity = intensity;
	this.position = position;
	this.direction = direction;
	this.theta = theta;
	this.phi = phi;
	this.falloff = falloff;
	this.shadow = true;
};

SpotLight.prototype.initialize = function() {
	this.s = this.direction.normalize().negate();
	this.cosTheta = Math.cos(this.theta * Math.PI / 180 / 2);
	this.cosPhi = Math.cos(this.phi * Math.PI / 180 / 2);
	this.baseMultiplier = 1 / (this.cosTheta - this.cosPhi);
};

SpotLight.prototype.sample = function(scene, position) {
	var delta = this.position.subtract(position);
	var rr = delta.sequare();
	var r = Math.sqrt(rr);
	var l = delta.divide(r);

	var spot;
	var SdotL = this.s.dot(l);
	if(SdotL >= this.cosTheta) {
		spot = 1;
	} else if(SdotL <= this.cosPhi) {
		spot = 0;
	} else {
		spot = Math.pow((SdotL - this.cosPhi) * this.baseMultiplier, this.falloff);
	}

	if(this.shadow) {
		var ray = new Ray3(position, l);
		var result = scene.intersect(ray);
		if(result.geometry && result.distance <= r) {
			return LightSample.zero;
		}
	}
	return new LightSample(l, this.intensity.multiply(spot / rr));
};



var Union = function(geometries) {
	this.geometries = geometries;
};

Union.prototype.initialize = function() {
	for(var i in this.geometries) {
		this.geometries[i].initialize();
	}
};

Union.prototype.intersect = function(ray) {
	var minDistance = Infinity;
	var minResult = IntersectResult.noHit;
	for(var i in this.geometries) {
		var result = this.geometries[i].intersect(ray);
		if(result.geometry && result.distance < minDistance){
			minDistance = result.distance;
			minResult = result
		}
	}
	return minResult;
};

var UnionLight = function(lights) {
	this.lights = lights;
};

UnionLight.prototype.initialize  = function() {
	for(var i in this.lights) {
		this.lights[i].initialize();
	}
};

UnionLight.prototype.sample = function(scene, position, normal) {
	var color = Color.black;
	var lightSample;
	for(var i in this.lights) {
		lightSample = this.lights[i].sample(scene, position);
		if(lightSample != LightSample.zero) {
			var NdotL = normal.dot(lightSample.l);
			if(NdotL >= 0){
				color = color.add(lightSample.el.multiply(NdotL));
			}
		}
	}
	return color;
};









	
function test(scene, camera, maxDepth) {
	
	scene.initialize();
	camera.initialize();

	var sx, sy;
	var ray, result, color;
	var i = 0;
	var qua = 6;

fs.createReadStream('test1.png')
.pipe(new PNG({}))
.on('parsed', function() {
	for (var y = 0; y < this.height; y++) {
		for (var x = 0; x < this.width; x++) {
			var i = (this.width * y + x) << 2;

			var c = Color.black;
			for(var a=0;a<qua;a++){
				for(var b=0;b<qua;b++){
					ray = camera.generateRay(2 * (x+b/qua) / this.width - 1, 2 * (y+a/qua) / this.height - 1);
					var color = rayReflex(scene, ray, 1, 6);
					c = c.add(color);
				}
			}
			this.data[i] = Math.min(c.r / qua / qua, 1) * 255;
			this.data[i + 1] = Math.min(c.g / qua / qua, 1) * 255;
			this.data[i + 2] = Math.min(c.b / qua / qua, 1) * 255;
			this.data[i + 3] = 255;
			i += 4;
		}
	}
	this.pack().pipe(fs.createWriteStream('out1.png'));
});


}
function rayReflex(scene, ray, prindex, maxDepth) {
	var c = Color.black;
	if(maxDepth <= 0) {
		return c;
	}
	var result = scene.intersect(ray);
	if(!result.geometry) {
		var light, result;
		for(var i = 0; i < lights.length; i++) {
			light = lights[i]
			result = light.intersect(ray);
			if(result.geometry) {
				return new Color(1, 1, 1);
			}
		}
		return c;
	}
	var shade, light;
	var color = result.geometry.material.sample(ray, result.position, result.normal, result.geometry);
	for(var i = 0; i < lights.length; i++) {
		light = lights[i]
		shade = light.shade(scene, result.position);
		light.intersect(ray);
		var diffuse = result.geometry.material.diffuse;
		var L = light.l.subtract(result.position).normalize();
		var NdotL = result.normal.dot(L);
		var diffuseTemp = color.multiply(Math.max(NdotL * shade * diffuse, 0));
		c = c.add(light.el.modulate(diffuseTemp));
		
		var specular = result.geometry.material.specular;
		var R =  L.subtract(result.normal.multiply(2 * NdotL));
		var NdotR = ray.direction.dot(R);
		if(NdotR > 0) {
			var secularTemp = Math.pow(NdotR, 20) * specular * shade;
			c = c.add(light.el.multiply(secularTemp));
		}
	}

	var reflect = result.geometry.material.reflect;
	if(reflect > 0) {
		var r = result.normal.multiply(-2 * result.normal.dot(ray.direction)).add(ray.direction);
		var flexRay = new Ray3(result.position, r);
		var reflectedColor = rayReflex(scene, flexRay, prindex, maxDepth - 1);
		c = c.add(reflectedColor.multiply(reflect).modulate(color));
	}

	var refract = result.geometry.material.refract;
	if(reflect > 0) {
		var rindex = result.geometry.material.rindex;
		if(rindex <= 0) {
			return c;
		}
		if(result.dir == -1) {
			rindex = 1;
		}
		var n = prindex / rindex;
		var cosI = -(result.normal.dot(ray.direction));
		var cosT2 =1 - Math.pow(n, 2) *(1 - Math.pow(cosI, 2));
		if (cosT2 > 0) {
			var cosT = Math.sqrt(cosT2);
			var T = ray.direction.multiply(n).add(result.normal.multiply(n * cosI - cosT));
			var fractRay = new Ray3(result.position, T);
			var rcol = rayReflex(scene, fractRay, rindex, maxDepth - 1);
			c = c.add(rcol.multiply(refract));
		}
	}

	return c;
}

var areaLight = new AreaLight(new Color(1, 1, 1), new Vector3(0.3 , 1.2 , 0.8), new Vector3(0.6 , 1.2 , 0.8), new Vector3(0.6 , 1.5 , 0.8), new Vector3(0.3 , 1.5 , 0.8));

var l1 = new LightSample(new Vector3(-5 , -5 , 1), new Color(0.3 , 0.3 , 0.3));
var l2 = new LightSample(new Vector3(-5 , 5 , 1), new Color(0.3 , 0.3 , 0.3));
var l3 = new LightSample(new Vector3(5 , -5 , 1), new Color(0.3 , 0.3 , 0.3));
var l4 = new LightSample(new Vector3(5 , 5 , 1), new Color(0.3 , 0.3 , 0.3));
var l5 = new LightSample(new Vector3(-5 , 0 , 1), new Color(0.3 , 0.3 , 0.3));
var l6 = new LightSample(new Vector3(0 , 5 , 1), new Color(0.3 , 0.3 , 0.3));
var l7 = new LightSample(new Vector3(5 , 0 , 1), new Color(0.3 , 0.3 , 0.3));
var l8 = new LightSample(new Vector3(0 , 5 , 1), new Color(0.3 , 0.3 , 0.3));
var lights = [areaLight,l1,l2,l3,l4,l5,l6,l7,l8];

var s1 = new Sphere(new Vector3(0.5, 1, 0.2), 0.2);
s1.material = new PhoneMaterial(0, 0.3, 0.3, 0.7, Color.white, 1.05);

var s2 = new Sphere(new Vector3(0, 1, 0.2), 0.2);
s2.material = new PhoneMaterial(0.2, 0.05, 0.75, 0, new Color(0, 1, 1));

var s3 = new Sphere(new Vector3(0.4, 1.7 , 0.2), 0.2);
s3.material = new PhoneMaterial(0.2, 0.05, 0.75, 0, new Color( 1, 0, 1));

var plane = new Plane(new Vector3(0, 0, 1), 0);
plane.material = new CheckerMaterial(0.2, 0.2, 0.6, 0, 10 / 3);

var camera = new PerspectiveCamera(new Vector3(-0.25 , -0.4 , 1), new Vector3(0.4 , 1 , -0.7), new Vector3(0,0,1), 90);
test(new Union([plane, s1, s2, s3]), camera, 50);